package com.dubious.interview.euler;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

public class Problem19 {
    
    private static int monday = 0;
    private static int sunday = 6;

    private static int seedYear = 1900;
    private static YearLookup seedYearLookup = new YearLookup(monday, isLeapYear(1900));
    
    /**
     * How many Sundays fell on the first of the month during the twentieth century (1 Jan 1901 to Dec 31 2000).
     */
    public static int run(int beginYear, int endYear)
    {
        // strategy:
        // there are the following variables for each year X:
        //     - on which day of the week does Jan 1 X fall?
        //     - is it a leap year?
        // I would suggest calculating, based on these two variables two things:
        //     - how many sundays would fall on first of the month in year X
        //     - what day of the week does Jan 1 X+1 fall on?
        // This gives us a function we can use for each year, rather than have to calculate it repeatedly for similar years
        
        // ensure beginYear is after our date seed
        if(seedYear >= beginYear)
        {
            throw new RuntimeException("Begin year [" + beginYear + "] is not after seed year [" + seedYear + "]");
        }
        // ensure endYear is after beginYear
        if(beginYear >= endYear)
        {
            throw new RuntimeException("End year [" + endYear + "] is not after begin year [" + beginYear + "]");
        }
        
        preCalculateYearResults();
        
        // iterate over years until we get to the beginYear
        int currentYear = seedYear;
        YearResult currentYearResult = getYearResult(seedYearLookup);
        while(currentYear < beginYear)
        {
            currentYear++;
            currentYearResult = getYearResult(new YearLookup(currentYearResult.nextYearJanuaryOneDayOfTheWeek, isLeapYear(currentYear)));
        }
        
        // currentYear is now beginYear
        int numSundaysOnFirstOfMonth = currentYearResult.numSundaysOnFirstOfMonth;
        while(currentYear < endYear)
        {
            currentYear++;
            currentYearResult = getYearResult(new YearLookup(currentYearResult.nextYearJanuaryOneDayOfTheWeek, isLeapYear(currentYear)));
            numSundaysOnFirstOfMonth += currentYearResult.numSundaysOnFirstOfMonth;
        }
        
        return numSundaysOnFirstOfMonth;
    }
    
    private static int[] daysInAMonth = new int[] {
        31, // January
        0,  // February is special
        31, // March
        30, // April
        31, // May
        30, // June
        31, // July
        31, // August
        30, // September
        31, // October
        30, // November
        31  // December
    };
    
    private static class YearLookup
    {
        private int januaryOneDayOfTheWeek;
        private boolean isLeapYear;
        
        private YearLookup(int januaryOneDayOfTheWeek, boolean isLeapYear)
        {
            this.januaryOneDayOfTheWeek = januaryOneDayOfTheWeek;
            this.isLeapYear = isLeapYear;
        }
        
        @Override
        public boolean equals(Object other)
        {
            return EqualsBuilder.reflectionEquals(this, other);
        }
        
        @Override
        public int hashCode()
        {
            return HashCodeBuilder.reflectionHashCode(this);
        }
        
        @Override
        public String toString()
        {
            return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
        }
    }
    
    private static class YearResult
    {
        private int numSundaysOnFirstOfMonth;
        private int nextYearJanuaryOneDayOfTheWeek;
        
        public YearResult(int numSundaysOnFirstOfMonth, int nextYearJanuaryOneDayOfTheWeek)
        {
            this.numSundaysOnFirstOfMonth = numSundaysOnFirstOfMonth;
            this.nextYearJanuaryOneDayOfTheWeek = nextYearJanuaryOneDayOfTheWeek;
        }
        
        @Override
        public String toString()
        {
            return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
        }
    }
    
    private static Map<YearLookup, YearResult> yearResults = new HashMap<YearLookup, YearResult>();
    
    private static void preCalculateYearResults()
    {
        for(int i = monday; i <= sunday; i++)
        {
            YearLookup yearLookupForLeapYear = new YearLookup(i, true);
            yearResults.put(yearLookupForLeapYear, calculateYearResult(yearLookupForLeapYear));
            
            YearLookup yearLookupForNonLeapYear = new YearLookup(i, false);
            yearResults.put(yearLookupForNonLeapYear, calculateYearResult(yearLookupForNonLeapYear));

//            System.out.println("day [" + i + "], isLeapYear [true] = " + yearResults.get(yearLookupForLeapYear));
//            System.out.println("day [" + i + "], isLeapYear [false] = " + yearResults.get(yearLookupForNonLeapYear));
        }
    }
    
    private static YearResult calculateYearResult(YearLookup yearLookup)
    {
        int currentFirstOfMonth = yearLookup.januaryOneDayOfTheWeek;
        int numSundaysOnFirstOfMonth = 0;
        if(currentFirstOfMonth == sunday)
        {
            numSundaysOnFirstOfMonth++;
        }
        
        for(int i = 0; i < 11; i++)
        {
            currentFirstOfMonth = (currentFirstOfMonth + getDaysInAMonth(i, yearLookup.isLeapYear)) % 7;
            if(currentFirstOfMonth == sunday)
            {
                numSundaysOnFirstOfMonth++;
            }
        }
        
        return new YearResult(numSundaysOnFirstOfMonth, (currentFirstOfMonth + getDaysInAMonth(11, yearLookup.isLeapYear)) % 7);
    }
    
    private static YearResult getYearResult(YearLookup yearLookup)
    {
        return yearResults.get(yearLookup);
    }
    
    private static boolean isLeapYear(int year)
    {
        // A leap year occurs on any year evenly divisible by 4, but not on a century unless it is divisible by 400
        return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0);
    }
    
    private static int getDaysInAMonth(int month, boolean isLeapYear)
    {
        if(month != 1)
        {
            return daysInAMonth[month];
        }
        
        // february is special
        if(isLeapYear)
        {
            return 29;
        } else
        {
            return 28;
        }
    }
}
